Skip to content

feat: add agent session sync - #48

Merged
ch-liuzhide merged 1 commit into
mainfrom
codex/agent-session-sync
Jul 2, 2026
Merged

feat: add agent session sync#48
ch-liuzhide merged 1 commit into
mainfrom
codex/agent-session-sync

Conversation

@ch-liuzhide

@ch-liuzhide ch-liuzhide commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add Agent Sync discovery, dedupe, and sync APIs for Claude Code and Codex local transcripts
  • add Web Console Agent Sync workflow with host switching, session status, and pending-turn sync actions
  • add hebb agent-sync CLI parity plus public EN/ZH docs and internal design notes

Testing

  • uv run ruff check src
  • uv run mypy src/hebb/
  • uv run pytest tests/ -q --tb=short
  • npm run docs:build from repo_pages

Summary by CodeRabbit

  • New Features

    • Added Agent Sync in the Web Console and CLI to view pending Claude Code/Codex sessions and import them into shared memory.
    • Added a session sync API plus new sidebar/navigation entries for the feature.
    • Added support for listing sync status, previewing imports, and syncing selected sessions.
  • Documentation

    • Updated English and Chinese docs, quick-start guides, API reference, and homepage content to cover Agent Sync workflows.
  • Bug Fixes

    • Improved session parsing and metadata handling so synced history is displayed and deduplicated more reliably.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces "Agent Sync," a feature bridging Codex and Claude Code session history into Hebb Mind. It adds transcript multi-turn extraction, a session discovery/normalization module, a FastAPI router with sessions/sync endpoints, a hebb agent-sync CLI, a web console page, and documentation. A separate change improves CLI command resolution to prefer in-repo source checkouts.

Changes

Agent Sync Feature

Layer / File(s) Summary
Transcript parsing
src/hebb/integrations/claude_code/transcript.py, src/hebb/integrations/codex/transcript.py, tests/unit/integrations/test_codex_hooks.py
Adds TurnRecord/extract_turns for Claude Code and session metadata (session_id, cwd) plus context-message filtering for Codex; adjusts a timestamp assertion.
Session discovery and memory conversion
src/hebb/integrations/session_sync.py, tests/unit/integrations/test_agent_session_sync.py
Discovers Codex/Claude Code transcripts, normalizes them into AgentSession/AgentTurn, converts turns into MemoryCreate objects with dedupe keys and fingerprints, and covers this with unit tests.
Agent Sync server API
src/hebb/server/routers/agent_sync.py, src/hebb/server/app.py, tests/integration/server/test_agent_sync_router.py
Adds GET /sessions and POST /sync endpoints computing sync state, embedding and persisting pending turns, mounted under /api/v1/agent-sync, with integration tests.
Agent Sync CLI
src/hebb/cli/commands/agent_sync.py, src/hebb/cli/main.py, tests/unit/cli/commands/test_agent_sync.py
Adds hebb agent-sync list/sync commands with host filtering, dry-run, JSON/table output, error handling, and registers the command group.
Web console Agent Sync page
src/hebb/static/js/components/agent-sync.js, src/hebb/static/js/api.js, src/hebb/static/js/app.js, src/hebb/static/index.html, src/hebb/static/css/style.css, src/hebb/static/js/i18n.js
Adds an Agent Sync page (session list, sync actions), API client functions, routing/aliasing, sidebar nav entry replacing "CC Memory", styling, and translations.
Agent Sync docs and reports
repo_pages/guide/agent-sync.md, repo_pages/zh/guide/agent-sync.md, repo_pages/api/cli.md, repo_pages/zh/api/cli.md, repo_pages/guide/web-console.md, repo_pages/zh/guide/web-console.md, repo_pages/quick-start.md, repo_pages/zh/quick-start.md, README.md, README_ZH.md, repo_pages/index.md, repo_pages/zh/index.md, repo_pages/.vitepress/config.mts, repo_pages/public/llms.txt, reports/analysis/*, reports/design/*
Adds new Agent Sync guides, CLI docs, updated feature descriptions, sidebar/nav entries, and design/analysis reports.

CLI Source-Checkout Command Resolution

Layer / File(s) Summary
Source-checkout resolution helpers
src/hebb/utils/cli_paths.py, tests/unit/utils/test_cli_paths.py, tests/unit/integrations/test_claude_code_hooks.py
Adds _source_checkout_command, _source_checkout_root, and _preferred_python helpers so hebb_command()/hebb_mcp_command() prefer running from the current repo checkout before falling back to installed entrypoints; adds tests and fixes hook test working-directory setup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebConsole
  participant CLI
  participant AgentSyncRouter
  participant SessionSync
  participant MemoryStore
  participant Embedder
  WebConsole->>AgentSyncRouter: GET /api/v1/agent-sync/sessions
  CLI->>AgentSyncRouter: POST /api/v1/agent-sync/sync
  AgentSyncRouter->>SessionSync: discover_sessions(host, limit)
  AgentSyncRouter->>MemoryStore: fetch existing turn keys
  AgentSyncRouter->>Embedder: batch embed pending turns
  AgentSyncRouter->>MemoryStore: store new memories
Loading

Possibly related PRs

  • afx-team/hebb-mind#47: Both PRs touch Codex transcript parsing and session/turn metadata used for dedupable memory writes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Agent Session Sync support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/agent-session-sync

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the 'Agent Sync' feature, establishing Hebb Mind as a shared memory hub for Claude Code and Codex by parsing, collecting, and syncing local session histories into the database. It adds corresponding Web Console pages, a new hebb agent-sync CLI command group, and server API endpoints. The review feedback highlights several critical performance and robustness improvements, such as offloading synchronous file I/O to a thread pool to avoid blocking the FastAPI event loop, chunking large embedding requests to prevent OOM errors, adding defensive checks for missing metadata or directories, and formatting raw epoch timestamps into human-readable dates in the CLI.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

store: MemoryStore = Depends(get_memory_store),
) -> list[AgentSessionOut]:
"""List local Codex and Claude Code sessions with sync status."""
sessions = session_sync.discover_sessions(host=host, limit=limit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The session_sync.discover_sessions function performs synchronous file I/O (directory scanning and file reading). Calling it directly inside an async def path operation blocks the FastAPI event loop, which can severely degrade performance under concurrent load. We should run it in an external thread pool using asyncio.to_thread.run.

Suggested change
sessions = session_sync.discover_sessions(host=host, limit=limit)
import asyncio
sessions = await asyncio.to_thread.run(session_sync.discover_sessions, host, limit)

embedder: EmbeddingProvider = Depends(get_embedder),
) -> AgentSyncResponse:
"""Sync local Codex and Claude Code session turns into Hebb Mind."""
sessions = session_sync.discover_sessions(host=request.host, limit=request.limit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to list_sessions, session_sync.discover_sessions is a synchronous I/O-bound function and should be run in a thread pool using asyncio.to_thread.run to avoid blocking the FastAPI event loop.

    import asyncio
    sessions = await asyncio.to_thread.run(session_sync.discover_sessions, request.host, request.limit)

Comment on lines +114 to +119
embeddings = await embedder.embed_batch([memory.content for _, _, memory in pending])
if len(embeddings) != len(pending):
raise HTTPException(
status_code=502,
detail=f"Embedder returned {len(embeddings)} vectors for {len(pending)} imported turns",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Generating embeddings for all pending turns in a single batch can lead to Out-Of-Memory (OOM) errors or exceed API payload/rate limits if the history is large. We should chunk the pending list and generate embeddings in smaller batches (e.g., 128 items at a time).

    batch_size = 128
    embeddings = []
    for i in range(0, len(pending), batch_size):
        batch = pending[i : i + batch_size]
        batch_embeddings = await embedder.embed_batch([memory.content for _, _, memory in batch])
        if len(batch_embeddings) != len(batch):
            raise HTTPException(
                status_code=502,
                detail=f"Embedder returned {len(batch_embeddings)} vectors for {len(batch)} imported turns",
            )
        embeddings.extend(batch_embeddings)

Comment on lines +149 to +150
def _metadata_dict(memory: Memory) -> dict[str, object]:
return memory.metadata.model_dump(exclude_none=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If a memory in the database does not have any metadata (i.e., memory.metadata is None), calling model_dump() will raise an AttributeError. We should add a defensive check to return an empty dictionary if metadata is None.

Suggested change
def _metadata_dict(memory: Memory) -> dict[str, object]:
return memory.metadata.model_dump(exclude_none=True)
def _metadata_dict(memory: Memory) -> dict[str, object]:
if memory.metadata is None:
return {}
return memory.metadata.model_dump(exclude_none=True)

Comment on lines +156 to +158
def _codex_session_paths() -> list[Path]:
home = _codex_home()
candidates: list[Path] = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with _claude_session_paths(), we should add a defensive check to ensure that the home directory exists and is indeed a directory before attempting to glob files. This prevents potential issues if the directory does not exist.

def _codex_session_paths() -> list[Path]:
    home = _codex_home()
    if not home.is_dir():
        return []
    candidates: list[Path] = []

Comment on lines +136 to +147
for session in sessions:
turn_count = int(session.get("turn_count") or 0)
synced = int(session.get("synced_turns") or 0)
pending = int(session.get("unsynced_turns") or 0)
table.add_row(
_host_label(str(session.get("host") or "")),
str(session.get("project") or "-"),
f"{synced}/{turn_count}",
str(pending),
str(session.get("latest_timestamp") or session.get("updated_at") or "-"),
str(session.get("id") or "-"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If latest_timestamp is missing, the table falls back to updated_at, which is a raw float (epoch seconds). Printing raw floats in a CLI table is not user-friendly. We should format it into a human-readable date string.

    for session in sessions:
        turn_count = int(session.get("turn_count") or 0)
        synced = int(session.get("synced_turns") or 0)
        pending = int(session.get("unsynced_turns") or 0)
        
        updated_val = session.get("latest_timestamp")
        if not updated_val and session.get("updated_at"):
            from datetime import datetime
            try:
                updated_val = datetime.fromtimestamp(float(session["updated_at"])).strftime("%Y-%m-%d %H:%M")
            except (ValueError, TypeError):
                updated_val = str(session["updated_at"])
                
        table.add_row(
            _host_label(str(session.get("host") or "")),
            str(session.get("project") or "-"),
            f"{synced}/{turn_count}",
            str(pending),
            str(updated_val or "-"),
            str(session.get("id") or "-"),
        )

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
src/hebb/integrations/claude_code/transcript.py (1)

140-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing Raises docstring section on extract_turns.

extract_turns calls _load_main_messages, which is documented to raise OSError, without catching it — so the exception propagates to callers. The docstring only has Args/Returns, unlike _load_main_messages itself and the sibling extract_turns in codex/transcript.py, which both document Raises: OSError. Callers of this API (e.g. session_sync._parse_turns) currently handle OSError, but the contract should be documented here too.

📝 Proposed docstring fix
     Returns:
         Parsed turn records in transcript order. Low-signal user prompts and
         incomplete turns without assistant output are omitted.
+
+    Raises:
+        OSError: If the transcript file exists but cannot be read.
     """

As per coding guidelines: "Include docstring with Args, Returns, and Raises sections for all public APIs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/integrations/claude_code/transcript.py` around lines 140 - 198, Add
a Raises section to the public API docstring for extract_turns to document that
OSError can propagate from _load_main_messages. Update the extract_turns
docstring alongside Args and Returns so it matches the contract used by
_load_main_messages and the sibling extract_turns in codex/transcript.py,
without changing the function logic.

Source: Coding guidelines

tests/integration/server/test_agent_sync_router.py (1)

43-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded /tmp/repo instead of tmp_path.

The sibling test file (test_agent_session_sync.py) uses str(tmp_path / "repo") for the same field; this test hardcodes "/tmp/repo" instead.

🧹 Proposed fix
                         "payload": {"id": "session-a", "cwd": "/tmp/repo"},
+                        "payload": {"id": "session-a", "cwd": str(home / "repo")},

As per coding guidelines: "MUST NOT hardcode API keys, secrets, or absolute paths outside the user's workspace."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/server/test_agent_sync_router.py` around lines 43 - 62, The
session fixture in _write_codex_session hardcodes the cwd payload to an absolute
/tmp/repo path, which should be replaced with the test’s temporary workspace
path. Update the session_meta payload in test_agent_sync_router.py to use the
provided tmp_path-based repo location, matching the approach used in
test_agent_session_sync.py and keeping the cwd inside the user workspace.

Sources: Coding guidelines, Linters/SAST tools

src/hebb/server/routers/agent_sync.py (2)

135-146: 🚀 Performance & Scalability | 🔵 Trivial

Full hippocampus partition scan on every /sessions and /sync call.

_existing_turn_keys loads and iterates all memories in HIPPOCAMPUS_PARTITION on each request, including the GET /sessions list endpoint that the web console likely polls. This will get slower as synced memories accumulate.

Consider an indexed/queryable lookup by (host, session_id, turn) metadata (e.g. a store-level filter) instead of a full partition materialization, if partition sizes are expected to grow large.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/server/routers/agent_sync.py` around lines 135 - 146, The
`_existing_turn_keys` helper is doing a full `HIPPOCAMPUS_PARTITION` scan via
`store.get_by_partition` on every `/sessions` and `/sync` request, which will
not scale as memories grow. Update this path to use a more targeted lookup in
`MemoryStore` based on the `(host, session_id, turn)` metadata, ideally by
adding or reusing a store-level filter/indexed query instead of materializing
the entire partition. Keep the existing `session_sync.turn_key` and
`_metadata_dict` flow, but change `_existing_turn_keys` to retrieve only
matching records rather than iterating all memories.

91-97: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Dedup check doesn't guard against in-batch collisions across sessions.

existing is only updated after a turn is actually persisted (line 130), not when it's added to pending here. If two discovered AgentSessions ever share the same (host, session_id, turn) (e.g. a transcript split across two files but keeping the same session id), both entries pass this check and both get queued and persisted, defeating the dedup purpose designed by turn_key.

♻️ Proposed fix
     for session in sessions:
         skipped = 0
         for turn in session.turns:
             if _has_existing(existing, session.host, session.session_id, turn.turn):
                 skipped += 1
                 continue
+            existing.add(session_sync.turn_key(session.host, session.session_id, turn.turn))
             pending.append((session, turn, session_sync.to_memory_create(session, turn)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/server/routers/agent_sync.py` around lines 91 - 97, The dedup logic
in the session turn collection loop does not prevent collisions within the same
batch because `existing` is only updated after persistence, so duplicate `(host,
session_id, turn)` entries from different `AgentSession`s can both be queued.
Update the `pending`-building flow in `agent_sync` to mark each accepted turn as
reserved immediately after `_has_existing` passes, using the same
`turn_key`/`existing` tracking used later on persist, so later sessions in the
same run will skip already-queued turns.
src/hebb/static/css/style.css (1)

803-809: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deprecated word-break: break-word value.

Stylelint flags this as deprecated. Prefer overflow-wrap: anywhere for wrapping long titles/names.

🎨 Proposed fix
 .agent-flow-title,
 .agent-hub-name {
   font-size: 17px;
   font-weight: 700;
   color: var(--text-primary);
-  word-break: break-word;
+  overflow-wrap: anywhere;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/static/css/style.css` around lines 803 - 809, The title/name styling
in the `.agent-flow-title` and `.agent-hub-name` rule uses the deprecated
`word-break: break-word` value; update that CSS block to use `overflow-wrap:
anywhere` instead so long labels still wrap correctly. Keep the change localized
to the shared selector rule in the stylesheet and remove the deprecated
property.

Source: Linters/SAST tools

src/hebb/static/js/components/agent-sync.js (3)

61-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

load() fetches all sessions with no limit.

The API supports a limit (max 500 per the AgentSyncRequest/query schema), but load() never passes one, so the console always requests the full unfiltered session list. Over a long local history this could grow unbounded and slow the initial render.

⚡ Suggested fix
-    sessions = await api.listAgentSessions();
+    sessions = await api.listAgentSessions({ limit: 500 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/static/js/components/agent-sync.js` around lines 61 - 73, The load()
function in agent-sync.js fetches every agent session without any limit, which
can make initial rendering slow as history grows. Update load() to call
api.listAgentSessions() with an explicit limit value at or below the
AgentSyncRequest maximum (500), and keep the existing loading/error handling in
place so sessions are still assigned from the bounded result set.

75-89: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No in-flight guard on sync(); buttons stay clickable during a sync.

Neither the "Sync pending" button (line 225/247) nor the per-session "Sync" button (line 188) is disabled while a sync request is in flight — renderBody()'s syncAll.disabled check only accounts for loading (session discovery), not an active sync() call. Rapid clicks can fire overlapping POST /sync requests.

🔒 Suggested fix: track a `syncing` flag
 let hostFilter = '';
 let sessions = [];
 let loading = false;
+let syncing = false;
 async function sync(root, ids = []) {
+  if (syncing) return;
+  syncing = true;
+  renderBody(root);
   try {
     const resp = await api.syncAgentSessions({
       host: hostFilter || null,
       ids,
     });
     success(t('agent_sync.synced_ok', {
       created: resp.memories_created,
       skipped: resp.skipped_existing,
     }));
     await load(root);
   } catch (e) {
     error(`${t('agent_sync.sync_failed')}: ${e.message}`);
+  } finally {
+    syncing = false;
   }
 }
-  if (syncAll) syncAll.disabled = loading || pending === 0;
+  if (syncAll) syncAll.disabled = loading || syncing || pending === 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/static/js/components/agent-sync.js` around lines 75 - 89, The sync
action in `sync()` has no in-flight guard, so `Sync pending` and per-session
`Sync` can be clicked multiple times and trigger overlapping requests. Add a
`syncing` state flag alongside the existing `loading` logic in `renderBody()`
and set it around `api.syncAgentSessions()` in `sync()` so both the global and
row-level sync controls are disabled while a sync is running, then clear it in a
finally path after the request completes.

18-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared esc() helper. config-section.js already exports this function, and other components import it; keeping a separate copy here just adds another duplicate sanitizer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/static/js/components/agent-sync.js` around lines 18 - 24, The
agent-sync component currently defines its own esc() sanitizer instead of
reusing the shared helper. Remove the local esc() implementation in
agent-sync.js and import the exported esc() from config-section.js, following
the same pattern used by the other components so there is a single source of
truth for escaping.

Sources: Learnings, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@repo_pages/api/cli.md`:
- Around line 122-133: The CLI docs currently advertise unsupported flags and
values for the agent-sync commands, which do not exist in `hebb agent-sync` or
`src/hebb/cli/commands/agent_sync.py`. Update the usage block and options table
to match the real command surface by removing `--limit`, `--id`, and the
explicit `all` host value unless they are implemented, and keep only the
supported flags (`--host`, `--dry-run`, `--json`, `--url`) in the docs.
Reference the `agent-sync list` and `agent-sync sync` command descriptions so
the text stays consistent with the actual CLI behavior.

In `@repo_pages/zh/api/cli.md`:
- Around line 119-131: The `hebb agent-sync` CLI docs are describing unsupported
`--limit` and `--id` options that are not parsed by `agent_sync.py`. Update the
usage examples and option table in the `agent-sync` section to remove those
flags unless you also add them to the actual command implementation. Keep the
documented options aligned with the real parser in
`src/hebb/cli/commands/agent_sync.py` and the related `list`/`sync` command
behavior.

In `@src/hebb/cli/commands/agent_sync.py`:
- Around line 204-226: _update _fail_request in agent_sync.py to be annotated as
NoReturn instead of None, since it always exits via SystemExit(1) and never
returns. Use the _fail_request helper name and its existing exception-handling
paths to update the return type, and keep the raise SystemExit(1) behavior so
mypy can correctly treat the except branch as terminating and stop flagging
sessions/result as possibly unbound._

In `@src/hebb/server/routers/agent_sync.py`:
- Around line 122-130: The persist path in the `agent_sync` loop silently
swallows exceptions from `store.create()`, so failures are impossible to
diagnose. Update the `try/except` around `store.create(memory,
embedding=embedding)` to log the exception with enough context (for example the
current session/turn identifiers) before incrementing `item.failed`; keep the
counter update and `continue`, but make sure the logger records the error path
in the same `agent_sync` flow.

In `@src/hebb/utils/cli_paths.py`:
- Around line 60-81: The Windows branch in _source_checkout_command currently
omits the PYTHONPATH override, so the source checkout import behavior differs
from the POSIX path; update the function to propagate PYTHONPATH for both
branches, likely by returning the command together with an env override and
adjusting callers accordingly. Make sure the docstring for
_source_checkout_command matches the actual behavior, and add tests that cover
the os.name == "nt" case so the checkout-import contract is verified on Windows
too.

---

Nitpick comments:
In `@src/hebb/integrations/claude_code/transcript.py`:
- Around line 140-198: Add a Raises section to the public API docstring for
extract_turns to document that OSError can propagate from _load_main_messages.
Update the extract_turns docstring alongside Args and Returns so it matches the
contract used by _load_main_messages and the sibling extract_turns in
codex/transcript.py, without changing the function logic.

In `@src/hebb/server/routers/agent_sync.py`:
- Around line 135-146: The `_existing_turn_keys` helper is doing a full
`HIPPOCAMPUS_PARTITION` scan via `store.get_by_partition` on every `/sessions`
and `/sync` request, which will not scale as memories grow. Update this path to
use a more targeted lookup in `MemoryStore` based on the `(host, session_id,
turn)` metadata, ideally by adding or reusing a store-level filter/indexed query
instead of materializing the entire partition. Keep the existing
`session_sync.turn_key` and `_metadata_dict` flow, but change
`_existing_turn_keys` to retrieve only matching records rather than iterating
all memories.
- Around line 91-97: The dedup logic in the session turn collection loop does
not prevent collisions within the same batch because `existing` is only updated
after persistence, so duplicate `(host, session_id, turn)` entries from
different `AgentSession`s can both be queued. Update the `pending`-building flow
in `agent_sync` to mark each accepted turn as reserved immediately after
`_has_existing` passes, using the same `turn_key`/`existing` tracking used later
on persist, so later sessions in the same run will skip already-queued turns.

In `@src/hebb/static/css/style.css`:
- Around line 803-809: The title/name styling in the `.agent-flow-title` and
`.agent-hub-name` rule uses the deprecated `word-break: break-word` value;
update that CSS block to use `overflow-wrap: anywhere` instead so long labels
still wrap correctly. Keep the change localized to the shared selector rule in
the stylesheet and remove the deprecated property.

In `@src/hebb/static/js/components/agent-sync.js`:
- Around line 61-73: The load() function in agent-sync.js fetches every agent
session without any limit, which can make initial rendering slow as history
grows. Update load() to call api.listAgentSessions() with an explicit limit
value at or below the AgentSyncRequest maximum (500), and keep the existing
loading/error handling in place so sessions are still assigned from the bounded
result set.
- Around line 75-89: The sync action in `sync()` has no in-flight guard, so
`Sync pending` and per-session `Sync` can be clicked multiple times and trigger
overlapping requests. Add a `syncing` state flag alongside the existing
`loading` logic in `renderBody()` and set it around `api.syncAgentSessions()` in
`sync()` so both the global and row-level sync controls are disabled while a
sync is running, then clear it in a finally path after the request completes.
- Around line 18-24: The agent-sync component currently defines its own esc()
sanitizer instead of reusing the shared helper. Remove the local esc()
implementation in agent-sync.js and import the exported esc() from
config-section.js, following the same pattern used by the other components so
there is a single source of truth for escaping.

In `@tests/integration/server/test_agent_sync_router.py`:
- Around line 43-62: The session fixture in _write_codex_session hardcodes the
cwd payload to an absolute /tmp/repo path, which should be replaced with the
test’s temporary workspace path. Update the session_meta payload in
test_agent_sync_router.py to use the provided tmp_path-based repo location,
matching the approach used in test_agent_session_sync.py and keeping the cwd
inside the user workspace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 213d2b3d-09ef-4fd6-9d15-ba0c890ac229

📥 Commits

Reviewing files that changed from the base of the PR and between 39e9f62 and 4bd25fb.

📒 Files selected for processing (36)
  • AGENTS.md
  • README.md
  • README_ZH.md
  • repo_pages/.vitepress/config.mts
  • repo_pages/api/cli.md
  • repo_pages/guide/agent-sync.md
  • repo_pages/guide/web-console.md
  • repo_pages/index.md
  • repo_pages/public/llms.txt
  • repo_pages/quick-start.md
  • repo_pages/zh/api/cli.md
  • repo_pages/zh/guide/agent-sync.md
  • repo_pages/zh/guide/web-console.md
  • repo_pages/zh/index.md
  • repo_pages/zh/quick-start.md
  • reports/analysis/codex-claude-code-session-memory-analysis.md
  • reports/design/agent-session-sync-design.md
  • src/hebb/cli/commands/agent_sync.py
  • src/hebb/cli/main.py
  • src/hebb/integrations/claude_code/transcript.py
  • src/hebb/integrations/codex/transcript.py
  • src/hebb/integrations/session_sync.py
  • src/hebb/server/app.py
  • src/hebb/server/routers/agent_sync.py
  • src/hebb/static/css/style.css
  • src/hebb/static/index.html
  • src/hebb/static/js/api.js
  • src/hebb/static/js/app.js
  • src/hebb/static/js/components/agent-sync.js
  • src/hebb/static/js/i18n.js
  • src/hebb/utils/cli_paths.py
  • tests/integration/server/test_agent_sync_router.py
  • tests/unit/cli/commands/test_agent_sync.py
  • tests/unit/integrations/test_agent_session_sync.py
  • tests/unit/integrations/test_claude_code_hooks.py
  • tests/unit/utils/test_cli_paths.py

Comment thread repo_pages/api/cli.md
Comment on lines +122 to +133
hebb agent-sync list [--host all|claude-code|codex] [--limit 100] [--json] [--url URL]
hebb agent-sync sync [--host all|claude-code|codex] [--id SESSION_ID]... [--limit 100] [--dry-run] [--json] [--url URL]
```

| Option | Applies to | Description |
|--------|------------|-------------|
| `--host` | `list`, `sync` | Filter to one source. `claude-code` maps to the API host `claude_code`. |
| `--limit` | `list`, `sync` | Maximum sessions to scan. |
| `--id` | `sync` | Sync only specific opaque session ids returned by `list --json`. May be repeated. |
| `--dry-run` | `sync` | Report pending turns without writing memories. |
| `--json` | `list`, `sync` | Print the raw API payload for scripts. |
| `--url` | `list`, `sync` | Override the server URL, useful for dev servers on non-default ports. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove unsupported CLI flags from the docs.

The usage block/table advertises --limit, --id, and an explicit all host value, but src/hebb/cli/commands/agent_sync.py only exposes --host, --dry-run, --json, and --url. As written, users will copy flags that the command rejects. If these options are meant to ship, wire them through the CLI first; otherwise trim the docs to the real surface. To target all sessions today, omit --host.

Suggested doc correction
-hebb agent-sync list [--host all|claude-code|codex] [--limit 100] [--json] [--url URL]
-hebb agent-sync sync [--host all|claude-code|codex] [--id SESSION_ID]... [--limit 100] [--dry-run] [--json] [--url URL]
+hebb agent-sync list [--host claude-code|codex] [--json] [--url URL]
+hebb agent-sync sync [--host claude-code|codex] [--dry-run] [--json] [--url URL]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo_pages/api/cli.md` around lines 122 - 133, The CLI docs currently
advertise unsupported flags and values for the agent-sync commands, which do not
exist in `hebb agent-sync` or `src/hebb/cli/commands/agent_sync.py`. Update the
usage block and options table to match the real command surface by removing
`--limit`, `--id`, and the explicit `all` host value unless they are
implemented, and keep only the supported flags (`--host`, `--dry-run`, `--json`,
`--url`) in the docs. Reference the `agent-sync list` and `agent-sync sync`
command descriptions so the text stays consistent with the actual CLI behavior.

Comment thread repo_pages/zh/api/cli.md
Comment on lines +204 to +226
def _fail_request(url: str, exc: httpx.HTTPError) -> None:
"""Print a consistent daemon failure and exit.

Args:
url: Base server URL that failed.
exc: HTTPX exception raised by the request.

Raises:
SystemExit: Always exits with status 1.
"""
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code
console.print(f"[red]Agent Sync API failed at {url}[/] (HTTP {status})")
if status in (404, 405):
console.print(" The running Hebb Mind service may be older than this checkout.")
console.print(" Restart the Hebb Mind service so CLI and server use the same version.")
else:
console.print(f" {exc}")
else:
console.print(f"[red]Cannot reach {url}[/]")
console.print(f" {exc}")
console.print(" Install/start the background service: [cyan]hebb service install[/]")
raise SystemExit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

_fail_request should be typed NoReturn, not None.

_fail_request always raises SystemExit(1) and is documented as such, but its signature is -> None. Under mypy strict, this means sessions (Line 33) and result (Line 52) will be reported as possibly-unbound after the try/except block, since mypy can't infer that control flow never returns from the except branch.

🐛 Proposed fix
-import json
+import json
 from typing import Any
+from typing import NoReturn
...
-def _fail_request(url: str, exc: httpx.HTTPError) -> None:
+def _fail_request(url: str, exc: httpx.HTTPError) -> NoReturn:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _fail_request(url: str, exc: httpx.HTTPError) -> None:
"""Print a consistent daemon failure and exit.
Args:
url: Base server URL that failed.
exc: HTTPX exception raised by the request.
Raises:
SystemExit: Always exits with status 1.
"""
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code
console.print(f"[red]Agent Sync API failed at {url}[/] (HTTP {status})")
if status in (404, 405):
console.print(" The running Hebb Mind service may be older than this checkout.")
console.print(" Restart the Hebb Mind service so CLI and server use the same version.")
else:
console.print(f" {exc}")
else:
console.print(f"[red]Cannot reach {url}[/]")
console.print(f" {exc}")
console.print(" Install/start the background service: [cyan]hebb service install[/]")
raise SystemExit(1)
import json
from typing import Any
from typing import NoReturn
...
def _fail_request(url: str, exc: httpx.HTTPError) -> NoReturn:
"""Print a consistent daemon failure and exit.
Args:
url: Base server URL that failed.
exc: HTTPX exception raised by the request.
Raises:
SystemExit: Always exits with status 1.
"""
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code
console.print(f"[red]Agent Sync API failed at {url}[/] (HTTP {status})")
if status in (404, 405):
console.print(" The running Hebb Mind service may be older than this checkout.")
console.print(" Restart the Hebb Mind service so CLI and server use the same version.")
else:
console.print(f" {exc}")
else:
console.print(f"[red]Cannot reach {url}[/]")
console.print(f" {exc}")
console.print(" Install/start the background service: [cyan]hebb service install[/]")
raise SystemExit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/cli/commands/agent_sync.py` around lines 204 - 226, _update
_fail_request in agent_sync.py to be annotated as NoReturn instead of None,
since it always exits via SystemExit(1) and never returns. Use the _fail_request
helper name and its existing exception-handling paths to update the return type,
and keep the raise SystemExit(1) behavior so mypy can correctly treat the except
branch as terminating and stop flagging sessions/result as possibly unbound._

Source: Coding guidelines

Comment on lines +122 to +130
for (session, turn, memory), embedding in zip(pending, embeddings, strict=True):
item = item_by_id[session.id]
try:
await store.create(memory, embedding=embedding)
except Exception:
item.failed += 1
continue
item.memories_created += 1
existing.add(session_sync.turn_key(session.host, session.session_id, turn.turn))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silent failure on persist: no logging before incrementing failed.

Any exception from store.create() (including unexpected bugs, not just transient store errors) is swallowed with only a counter increment — there's no log trail to diagnose why turns failed to sync.

🪵 Proposed fix
+import logging
+
+logger = logging.getLogger(__name__)
+
...
         try:
             await store.create(memory, embedding=embedding)
         except Exception:
+            logger.exception(
+                "Failed to persist synced turn host=%s session=%s turn=%s",
+                session.host, session.session_id, turn.turn,
+            )
             item.failed += 1
             continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (session, turn, memory), embedding in zip(pending, embeddings, strict=True):
item = item_by_id[session.id]
try:
await store.create(memory, embedding=embedding)
except Exception:
item.failed += 1
continue
item.memories_created += 1
existing.add(session_sync.turn_key(session.host, session.session_id, turn.turn))
import logging
logger = logging.getLogger(__name__)
for (session, turn, memory), embedding in zip(pending, embeddings, strict=True):
item = item_by_id[session.id]
try:
await store.create(memory, embedding=embedding)
except Exception:
logger.exception(
"Failed to persist synced turn host=%s session=%s turn=%s",
session.host, session.session_id, turn.turn,
)
item.failed += 1
continue
item.memories_created += 1
existing.add(session_sync.turn_key(session.host, session.session_id, turn.turn))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/server/routers/agent_sync.py` around lines 122 - 130, The persist
path in the `agent_sync` loop silently swallows exceptions from
`store.create()`, so failures are impossible to diagnose. Update the
`try/except` around `store.create(memory, embedding=embedding)` to log the
exception with enough context (for example the current session/turn identifiers)
before incrementing `item.failed`; keep the counter update and `continue`, but
make sure the logger records the error path in the same `agent_sync` flow.

Comment on lines +60 to +81


def _source_checkout_command(module: str) -> list[str] | None:
"""Return a source-checkout command when running from this repository.

Args:
module: Python module to execute with ``-m``.

Returns:
Command argv with ``PYTHONPATH`` pointed at the checkout's ``src``
directory, or ``None`` when the current command is not being run from
this source tree.
"""
root = _source_checkout_root()
if root is None:
return None
src = root / "src"
python = _preferred_python(root)
if os.name == "nt":
return [str(python), "-m", module]
return ["/usr/bin/env", f"PYTHONPATH={src}", str(python), "-m", module]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Windows branch drops PYTHONPATH, breaking the checkout-import contract.

The POSIX branch wraps the command with /usr/bin/env PYTHONPATH=<src> ... so the checkout's src is importable even without an editable install, but the Windows branch (os.name == "nt") returns [str(python), "-m", module] with no equivalent mechanism to set PYTHONPATH. If the selected .venv python doesn't already have the package installed (the exact scenario this feature targets), running on Windows will raise ModuleNotFoundError for module. The docstring's claim that the returned command has "PYTHONPATH pointed at the checkout's src directory" is also inaccurate for this branch. This gap isn't covered by tests either — the unit tests only monkeypatch os.name to "posix".

🩹 Proposed fix
     if os.name == "nt":
-        return [str(python), "-m", module]
+        # Windows has no /usr/bin/env-style inline env var trick; callers must
+        # merge PYTHONPATH into the subprocess env themselves, or use setx-style
+        # invocation. As a minimal fix, at least surface this via a documented
+        # convention, e.g. returning a tuple of (argv, env) or requiring callers
+        # to call a companion `_source_checkout_env(root)` helper.
+        return [str(python), "-m", module]

Consider changing the return type to include the env override (e.g. tuple[list[str], dict[str, str]]) so both platforms propagate PYTHONPATH consistently, and updating callers/tests accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/utils/cli_paths.py` around lines 60 - 81, The Windows branch in
_source_checkout_command currently omits the PYTHONPATH override, so the source
checkout import behavior differs from the POSIX path; update the function to
propagate PYTHONPATH for both branches, likely by returning the command together
with an env override and adjusting callers accordingly. Make sure the docstring
for _source_checkout_command matches the actual behavior, and add tests that
cover the os.name == "nt" case so the checkout-import contract is verified on
Windows too.

@ch-liuzhide
ch-liuzhide force-pushed the codex/agent-session-sync branch from d8ac982 to 149e9ad Compare July 2, 2026 08:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/hebb/static/css/style.css (1)

803-809: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

word-break: break-word is deprecated.

Per MDN, break-word is a deprecated legacy keyword with "the same effect as overflow-wrap: anywhere combined with word-break: normal, regardless of the actual value of the overflow-wrap property." Consider migrating to the modern equivalent.

♻️ Suggested fix
 .agent-flow-title,
 .agent-hub-name {
   font-size: 17px;
   font-weight: 700;
   color: var(--text-primary);
-  word-break: break-word;
+  overflow-wrap: anywhere;
+  word-break: normal;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/static/css/style.css` around lines 803 - 809, The title styles in
agent-flow-title and agent-hub-name use the deprecated word-break: break-word
value; update these rules to the modern equivalent by using overflow-wrap:
anywhere together with word-break: normal so the text wrapping behavior stays
the same while removing the legacy keyword.

Source: Linters/SAST tools

tests/integration/server/test_agent_sync_router.py (1)

65-101: 🚀 Performance & Scalability | 🔵 Trivial

Direct function calls skip the HTTP/DI layer despite the "integration" test label.

Both tests call list_sessions/sync_sessions directly with hand-rolled fakes rather than going through the FastAPI app (e.g., via TestClient), so request validation, dependency wiring (store/embedder providers), and JSON (de)serialization at the route boundary aren't exercised. If there's no other test that hits these endpoints over HTTP, consider adding one for full contract coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/server/test_agent_sync_router.py` around lines 65 - 101,
The integration tests for list_sessions and sync_sessions are bypassing the
FastAPI route layer by calling the functions directly, so they do not cover
request validation, dependency injection, or JSON serialization at the HTTP
boundary. Update these tests to exercise the actual app endpoints through the
FastAPI test client, using the route handlers and their DI providers for store
and embedder, so the contract is validated end to end.
repo_pages/guide/web-console.md (1)

62-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a mermaid diagram for the Agent Sync data flow.

The section describes a cross-agent data flow (Source software → Hebb Mind → Available to Claude Code / Codex) in prose only, while the rest of the page uses mermaid diagrams for architecture/data flow (see lines 19-33). As per coding guidelines, "Use mermaid for architecture and data-flow diagrams (renders in VitePress and on GitHub)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo_pages/guide/web-console.md` around lines 62 - 72, Add a mermaid diagram
to the Agent Sync section to represent the cross-agent data flow instead of
leaving it only in prose. Update the content around the Agent Sync heading and
the source-to-Hebb Mind-to-Claude Code/Codex flow so it matches the existing
mermaid-based architecture diagrams used elsewhere on the page. Keep the
surrounding bullets and CLI references, and ensure the new diagram clearly shows
the sync path and available destinations.

Source: Coding guidelines

src/hebb/cli/commands/agent_sync.py (1)

20-59: 📐 Maintainability & Code Quality | 🔵 Trivial

Add the required docstring sections to the public CLI callbacks.

agent_sync_cmd, list_cmd, and sync_cmd are public Python APIs, but their docstrings only have a summary line. The repo guideline requires Args, Returns, and Raises sections for public APIs.

As per coding guidelines, **/*.py: Public Python APIs must have docstrings that include Args, Returns, and Raises.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/cli/commands/agent_sync.py` around lines 20 - 59, The public CLI
callbacks agent_sync_cmd, list_cmd, and sync_cmd only have summary docstrings,
but they must follow the Python API docstring standard. Update each docstring to
include Args for parameters like host, url, dry_run, and as_json, Returns for
the command’s None return, and Raises for any expected click/httpx-related
failures handled through _fail_request or propagated exceptions. Keep the
existing symbols and behavior unchanged while expanding the docstrings to
satisfy the repo guideline.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hebb/integrations/claude_code/transcript.py`:
- Around line 147-205: extract_turns currently lets OSError from
_load_main_messages escape, which can abort transcript processing instead of
skipping an unreadable file. Wrap the _load_main_messages call in extract_turns
with the same failure handling used by extract_last_turn, and return an empty
list or otherwise safely ignore the transcript when OSError occurs. Also update
the extract_turns docstring to include a Raises section documenting the OSError
behavior, keeping it consistent with the public API guidelines.

---

Nitpick comments:
In `@repo_pages/guide/web-console.md`:
- Around line 62-72: Add a mermaid diagram to the Agent Sync section to
represent the cross-agent data flow instead of leaving it only in prose. Update
the content around the Agent Sync heading and the source-to-Hebb Mind-to-Claude
Code/Codex flow so it matches the existing mermaid-based architecture diagrams
used elsewhere on the page. Keep the surrounding bullets and CLI references, and
ensure the new diagram clearly shows the sync path and available destinations.

In `@src/hebb/cli/commands/agent_sync.py`:
- Around line 20-59: The public CLI callbacks agent_sync_cmd, list_cmd, and
sync_cmd only have summary docstrings, but they must follow the Python API
docstring standard. Update each docstring to include Args for parameters like
host, url, dry_run, and as_json, Returns for the command’s None return, and
Raises for any expected click/httpx-related failures handled through
_fail_request or propagated exceptions. Keep the existing symbols and behavior
unchanged while expanding the docstrings to satisfy the repo guideline.

In `@src/hebb/static/css/style.css`:
- Around line 803-809: The title styles in agent-flow-title and agent-hub-name
use the deprecated word-break: break-word value; update these rules to the
modern equivalent by using overflow-wrap: anywhere together with word-break:
normal so the text wrapping behavior stays the same while removing the legacy
keyword.

In `@tests/integration/server/test_agent_sync_router.py`:
- Around line 65-101: The integration tests for list_sessions and sync_sessions
are bypassing the FastAPI route layer by calling the functions directly, so they
do not cover request validation, dependency injection, or JSON serialization at
the HTTP boundary. Update these tests to exercise the actual app endpoints
through the FastAPI test client, using the route handlers and their DI providers
for store and embedder, so the contract is validated end to end.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f23cf9b9-34b5-4033-b780-428f6c5a301b

📥 Commits

Reviewing files that changed from the base of the PR and between d8ac982 and 149e9ad.

📒 Files selected for processing (37)
  • AGENTS.md
  • README.md
  • README_ZH.md
  • repo_pages/.vitepress/config.mts
  • repo_pages/api/cli.md
  • repo_pages/guide/agent-sync.md
  • repo_pages/guide/web-console.md
  • repo_pages/index.md
  • repo_pages/public/llms.txt
  • repo_pages/quick-start.md
  • repo_pages/zh/api/cli.md
  • repo_pages/zh/guide/agent-sync.md
  • repo_pages/zh/guide/web-console.md
  • repo_pages/zh/index.md
  • repo_pages/zh/quick-start.md
  • reports/analysis/codex-claude-code-session-memory-analysis.md
  • reports/design/agent-session-sync-design.md
  • src/hebb/cli/commands/agent_sync.py
  • src/hebb/cli/main.py
  • src/hebb/integrations/claude_code/transcript.py
  • src/hebb/integrations/codex/transcript.py
  • src/hebb/integrations/session_sync.py
  • src/hebb/server/app.py
  • src/hebb/server/routers/agent_sync.py
  • src/hebb/static/css/style.css
  • src/hebb/static/index.html
  • src/hebb/static/js/api.js
  • src/hebb/static/js/app.js
  • src/hebb/static/js/components/agent-sync.js
  • src/hebb/static/js/i18n.js
  • src/hebb/utils/cli_paths.py
  • tests/integration/server/test_agent_sync_router.py
  • tests/unit/cli/commands/test_agent_sync.py
  • tests/unit/integrations/test_agent_session_sync.py
  • tests/unit/integrations/test_claude_code_hooks.py
  • tests/unit/integrations/test_codex_hooks.py
  • tests/unit/utils/test_cli_paths.py
✅ Files skipped from review due to trivial changes (10)
  • repo_pages/guide/agent-sync.md
  • README_ZH.md
  • reports/analysis/codex-claude-code-session-memory-analysis.md
  • src/hebb/static/js/i18n.js
  • repo_pages/zh/api/cli.md
  • repo_pages/quick-start.md
  • repo_pages/api/cli.md
  • README.md
  • repo_pages/zh/index.md
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (15)
  • repo_pages/public/llms.txt
  • repo_pages/.vitepress/config.mts
  • repo_pages/index.md
  • tests/unit/integrations/test_codex_hooks.py
  • src/hebb/static/js/api.js
  • tests/unit/utils/test_cli_paths.py
  • src/hebb/cli/main.py
  • src/hebb/server/app.py
  • src/hebb/static/index.html
  • src/hebb/utils/cli_paths.py
  • src/hebb/static/js/app.js
  • tests/unit/cli/commands/test_agent_sync.py
  • src/hebb/integrations/codex/transcript.py
  • src/hebb/integrations/session_sync.py
  • src/hebb/server/routers/agent_sync.py

Comment on lines +147 to +205
def extract_turns(transcript_path: str | Path) -> list[TurnRecord]:
"""Extract all complete user-to-assistant turns from a Claude Code JSONL transcript.

Args:
transcript_path: Path to the session ``.jsonl`` file.

Returns:
Parsed turn records in transcript order. Low-signal user prompts and
incomplete turns without assistant output are omitted.
"""
messages = _load_main_messages(Path(transcript_path))
if not messages:
return []

human_indices = [i for i, msg in enumerate(messages) if msg.get("type") == "user" and _raw_user_text(msg)]
records: list[TurnRecord] = []

for pos, user_idx in enumerate(human_indices):
user_msg = messages[user_idx]
user_text = _extract_user_text(user_msg)
if not user_text:
continue

next_user_idx = human_indices[pos + 1] if pos + 1 < len(human_indices) else len(messages)
segment = messages[user_idx + 1 : next_user_idx]

summary = TurnSummary(user_input=user_text, turn=pos)
for msg in segment:
if msg.get("type") == "assistant":
_extract_assistant(msg, summary, text=False)

for msg in reversed(segment):
if msg.get("type") == "assistant":
candidate = TurnSummary()
_extract_assistant(msg, candidate, text=True)
if candidate.assistant_output:
summary.assistant_output = candidate.assistant_output
break

summary.tools = _dedup(summary.tools)
summary.mcps = _dedup(summary.mcps)
if not summary.assistant_output:
continue

timestamp = user_msg.get("timestamp")
session_id = user_msg.get("sessionId") or user_msg.get("session_id")
cwd = user_msg.get("cwd")
records.append(
TurnRecord(
summary=summary,
timestamp=timestamp if isinstance(timestamp, str) else None,
session_id=session_id if isinstance(session_id, str) else None,
cwd=cwd if isinstance(cwd, str) else None,
)
)

return records


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

extract_turns doesn't handle OSError from _load_main_messages; missing Raises docstring section.

_load_main_messages documents (and does) re-raise OSError when a transcript exists but can't be read (lines 308-309, 331-333). extract_last_turn guards against this (lines 85-88, returns None), but extract_turns calls _load_main_messages directly at line 157 with no try/except. A single unreadable/permission-denied transcript will now raise uncaught through extract_claude_turns in session_sync.py, aborting the whole Agent Sync discovery batch rather than just skipping that file — inconsistent with the sibling function's failure mode.

The docstring at lines 147-156 also omits the Raises section required by project guidelines for public APIs.

🐛 Proposed fix
     Returns:
         Parsed turn records in transcript order. Low-signal user prompts and
         incomplete turns without assistant output are omitted.
+
+    Raises:
+        Nothing — read failures are caught and result in an empty list.
     """
-    messages = _load_main_messages(Path(transcript_path))
+    try:
+        messages = _load_main_messages(Path(transcript_path))
+    except OSError:
+        return []
     if not messages:
         return []

As per coding guidelines, "Include docstring with Args, Returns, and Raises sections for all public APIs."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def extract_turns(transcript_path: str | Path) -> list[TurnRecord]:
"""Extract all complete user-to-assistant turns from a Claude Code JSONL transcript.
Args:
transcript_path: Path to the session ``.jsonl`` file.
Returns:
Parsed turn records in transcript order. Low-signal user prompts and
incomplete turns without assistant output are omitted.
"""
messages = _load_main_messages(Path(transcript_path))
if not messages:
return []
human_indices = [i for i, msg in enumerate(messages) if msg.get("type") == "user" and _raw_user_text(msg)]
records: list[TurnRecord] = []
for pos, user_idx in enumerate(human_indices):
user_msg = messages[user_idx]
user_text = _extract_user_text(user_msg)
if not user_text:
continue
next_user_idx = human_indices[pos + 1] if pos + 1 < len(human_indices) else len(messages)
segment = messages[user_idx + 1 : next_user_idx]
summary = TurnSummary(user_input=user_text, turn=pos)
for msg in segment:
if msg.get("type") == "assistant":
_extract_assistant(msg, summary, text=False)
for msg in reversed(segment):
if msg.get("type") == "assistant":
candidate = TurnSummary()
_extract_assistant(msg, candidate, text=True)
if candidate.assistant_output:
summary.assistant_output = candidate.assistant_output
break
summary.tools = _dedup(summary.tools)
summary.mcps = _dedup(summary.mcps)
if not summary.assistant_output:
continue
timestamp = user_msg.get("timestamp")
session_id = user_msg.get("sessionId") or user_msg.get("session_id")
cwd = user_msg.get("cwd")
records.append(
TurnRecord(
summary=summary,
timestamp=timestamp if isinstance(timestamp, str) else None,
session_id=session_id if isinstance(session_id, str) else None,
cwd=cwd if isinstance(cwd, str) else None,
)
)
return records
def extract_turns(transcript_path: str | Path) -> list[TurnRecord]:
"""Extract all complete user-to-assistant turns from a Claude Code JSONL transcript.
Args:
transcript_path: Path to the session ``.jsonl`` file.
Returns:
Parsed turn records in transcript order. Low-signal user prompts and
incomplete turns without assistant output are omitted.
Raises:
Nothingread failures are caught and result in an empty list.
"""
try:
messages = _load_main_messages(Path(transcript_path))
except OSError:
return []
if not messages:
return []
human_indices = [i for i, msg in enumerate(messages) if msg.get("type") == "user" and _raw_user_text(msg)]
records: list[TurnRecord] = []
for pos, user_idx in enumerate(human_indices):
user_msg = messages[user_idx]
user_text = _extract_user_text(user_msg)
if not user_text:
continue
next_user_idx = human_indices[pos + 1] if pos + 1 < len(human_indices) else len(messages)
segment = messages[user_idx + 1 : next_user_idx]
summary = TurnSummary(user_input=user_text, turn=pos)
for msg in segment:
if msg.get("type") == "assistant":
_extract_assistant(msg, summary, text=False)
for msg in reversed(segment):
if msg.get("type") == "assistant":
candidate = TurnSummary()
_extract_assistant(msg, candidate, text=True)
if candidate.assistant_output:
summary.assistant_output = candidate.assistant_output
break
summary.tools = _dedup(summary.tools)
summary.mcps = _dedup(summary.mcps)
if not summary.assistant_output:
continue
timestamp = user_msg.get("timestamp")
session_id = user_msg.get("sessionId") or user_msg.get("session_id")
cwd = user_msg.get("cwd")
records.append(
TurnRecord(
summary=summary,
timestamp=timestamp if isinstance(timestamp, str) else None,
session_id=session_id if isinstance(session_id, str) else None,
cwd=cwd if isinstance(cwd, str) else None,
)
)
return records
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hebb/integrations/claude_code/transcript.py` around lines 147 - 205,
extract_turns currently lets OSError from _load_main_messages escape, which can
abort transcript processing instead of skipping an unreadable file. Wrap the
_load_main_messages call in extract_turns with the same failure handling used by
extract_last_turn, and return an empty list or otherwise safely ignore the
transcript when OSError occurs. Also update the extract_turns docstring to
include a Raises section documenting the OSError behavior, keeping it consistent
with the public API guidelines.

Source: Coding guidelines

@ch-liuzhide
ch-liuzhide merged commit b2656b3 into main Jul 2, 2026
19 checks passed
@ch-liuzhide
ch-liuzhide deleted the codex/agent-session-sync branch July 2, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant